1   // Copyright 2006, 2007, 2008, 2010, 2011 The Apache Software Foundation
2   //
3   // Licensed under the Apache License, Version 2.0 (the "License");
4   // you may not use this file except in compliance with the License.
5   // You may obtain a copy of the License at
6   //
7   // http://www.apache.org/licenses/LICENSE-2.0
8   //
9   // Unless required by applicable law or agreed to in writing, software
10  // distributed under the License is distributed on an "AS IS" BASIS,
11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12  // See the License for the specific language governing permissions and
13  // limitations under the License.
14  
15  package org.apache.tapestry5.internal.services;
16  
17  import org.apache.tapestry5.ComponentResources;
18  import org.apache.tapestry5.Field;
19  import org.apache.tapestry5.FieldValidator;
20  import org.apache.tapestry5.Validator;
21  import org.apache.tapestry5.ioc.MessageFormatter;
22  import org.apache.tapestry5.ioc.Messages;
23  import org.apache.tapestry5.ioc.internal.util.CollectionFactory;
24  import org.apache.tapestry5.ioc.internal.util.InternalUtils;
25  import org.apache.tapestry5.ioc.services.TypeCoercer;
26  import org.apache.tapestry5.runtime.Component;
27  import org.apache.tapestry5.services.FieldValidatorSource;
28  import org.apache.tapestry5.services.FormSupport;
29  import org.apache.tapestry5.validator.ValidatorMacro;
30  
31  import java.util.List;
32  import java.util.Locale;
33  import java.util.Map;
34  
35  import static org.apache.tapestry5.ioc.internal.util.CollectionFactory.newList;
36  
37  @SuppressWarnings("all")
38  public class FieldValidatorSourceImpl implements FieldValidatorSource
39  {
40      private final Messages globalMessages;
41  
42      private final Map<String, Validator> validators;
43  
44      private final TypeCoercer typeCoercer;
45  
46      private final FormSupport formSupport;
47  
48      private final ValidatorMacro validatorMacro;
49  
50      public FieldValidatorSourceImpl(Messages globalMessages, TypeCoercer typeCoercer,
51                                      FormSupport formSupport, Map<String, Validator> validators, ValidatorMacro validatorMacro)
52      {
53          this.globalMessages = globalMessages;
54          this.typeCoercer = typeCoercer;
55          this.formSupport = formSupport;
56          this.validators = validators;
57          this.validatorMacro = validatorMacro;
58      }
59  
60      public FieldValidator createValidator(Field field, String validatorType, String constraintValue)
61      {
62          Component component = (Component) field;
63          assert InternalUtils.isNonBlank(validatorType);
64          ComponentResources componentResources = component.getComponentResources();
65          String overrideId = componentResources.getId();
66  
67          // So, if you use a TextField on your EditUser page, we want to search the messages
68          // of the EditUser page (the container), not the TextField (which will always be the same).
69  
70          Messages overrideMessages = componentResources.getContainerMessages();
71  
72          return createValidator(field, validatorType, constraintValue, overrideId, overrideMessages, null);
73      }
74  
75      public FieldValidator createValidator(Field field, String validatorType, String constraintValue, String overrideId,
76                                            Messages overrideMessages, Locale locale)
77      {
78  
79          ValidatorSpecification originalSpec = new ValidatorSpecification(validatorType, constraintValue);
80  
81          List<ValidatorSpecification> org = CollectionFactory.newList(originalSpec);
82  
83          List<ValidatorSpecification> specs = expandMacros(org);
84  
85          List<FieldValidator> fieldValidators = CollectionFactory.<FieldValidator>newList();
86  
87          for (ValidatorSpecification spec : specs)
88          {
89              fieldValidators.add(createValidator(field, spec, overrideId, overrideMessages));
90          }
91  
92          return new CompositeFieldValidator(fieldValidators);
93      }
94  
95      private FieldValidator createValidator(Field field, ValidatorSpecification spec, String overrideId,
96                                             Messages overrideMessages)
97      {
98  
99          String validatorType = spec.getValidatorType();
100 
101         assert InternalUtils.isNonBlank(validatorType);
102         Validator validator = validators.get(validatorType);
103 
104         if (validator == null)
105             throw new IllegalArgumentException(String.format("Unknown validator type '%s'. Configured validators are %s.", validatorType, InternalUtils.join(InternalUtils.sortedKeys(validators))));
106 
107         // I just have this thing about always treating parameters as finals, so
108         // we introduce a second variable to treat a mutable.
109 
110         String formValidationid = formSupport.getFormValidationId();
111 
112         Object coercedConstraintValue = computeConstraintValue(validatorType, validator, spec.getConstraintValue(),
113                 formValidationid, overrideId, overrideMessages);
114 
115         MessageFormatter formatter = findMessageFormatter(formValidationid, overrideId, overrideMessages, validatorType,
116                 validator);
117 
118         return new FieldValidatorImpl(field, coercedConstraintValue, formatter, validator, formSupport);
119     }
120 
121     private Object computeConstraintValue(String validatorType, Validator validator, String constraintValue,
122                                           String formId, String overrideId, Messages overrideMessages)
123     {
124         Class constraintType = validator.getConstraintType();
125 
126         String constraintText = findConstraintValue(validatorType, constraintType, constraintValue, formId, overrideId,
127                 overrideMessages);
128 
129         if (constraintText == null)
130             return null;
131 
132         return typeCoercer.coerce(constraintText, constraintType);
133     }
134 
135     private String findConstraintValue(String validatorType, Class constraintType, String constraintValue,
136                                        String formValidationId, String overrideId, Messages overrideMessages)
137     {
138         if (constraintValue != null)
139             return constraintValue;
140 
141         if (constraintType == null)
142             return null;
143 
144         // If no constraint was provided, check to see if it is available via a localized message
145         // key. This is really handy for complex validations such as patterns.
146 
147         String perFormKey = formValidationId + "-" + overrideId + "-" + validatorType;
148 
149         if (overrideMessages.contains(perFormKey))
150             return overrideMessages.get(perFormKey);
151 
152         String generalKey = overrideId + "-" + validatorType;
153 
154         if (overrideMessages.contains(generalKey))
155             return overrideMessages.get(generalKey);
156 
157         throw new IllegalArgumentException(String.format("Validator '%s' requires a validation constraint (of type %s) but none was provided. The constraint may be provided inside the @Validator annotation on the property, or in the associated component message catalog as key '%s' or key '%s'.", validatorType, constraintType.getName(), perFormKey,
158                 generalKey));
159     }
160 
161     private MessageFormatter findMessageFormatter(String formId, String overrideId, Messages overrideMessages,
162                                                   String validatorType, Validator validator)
163     {
164 
165         String overrideKey = formId + "-" + overrideId + "-" + validatorType + "-message";
166 
167         if (overrideMessages.contains(overrideKey))
168             return overrideMessages.getFormatter(overrideKey);
169 
170         overrideKey = overrideId + "-" + validatorType + "-message";
171 
172         if (overrideMessages.contains(overrideKey))
173             return overrideMessages.getFormatter(overrideKey);
174 
175         String key = validator.getMessageKey();
176 
177         return globalMessages.getFormatter(key);
178     }
179 
180     public FieldValidator createValidators(Field field, String specification)
181     {
182         List<ValidatorSpecification> specs = toValidatorSpecifications(specification);
183 
184         List<FieldValidator> fieldValidators = CollectionFactory.newList();
185 
186         for (ValidatorSpecification spec : specs)
187         {
188             fieldValidators.add(createValidator(field, spec.getValidatorType(), spec.getConstraintValue()));
189         }
190 
191         if (fieldValidators.size() == 1)
192             return fieldValidators.get(0);
193 
194         return new CompositeFieldValidator(fieldValidators);
195     }
196 
197     List<ValidatorSpecification> toValidatorSpecifications(String specification)
198     {
199         return expandMacros(parse(specification));
200     }
201 
202     private List<ValidatorSpecification> expandMacros(List<ValidatorSpecification> specs)
203     {
204         Map<String, Boolean> expandedMacros = CollectionFactory.newCaseInsensitiveMap();
205         List<ValidatorSpecification> queue = CollectionFactory.newList(specs);
206         List<ValidatorSpecification> result = CollectionFactory.newList();
207 
208         while (!queue.isEmpty())
209         {
210             ValidatorSpecification head = queue.remove(0);
211 
212             String validatorType = head.getValidatorType();
213 
214             String expanded = validatorMacro.valueForMacro(validatorType);
215             if (expanded != null)
216             {
217                 if (head.getConstraintValue() != null)
218                     throw new RuntimeException(String.format(
219                             "'%s' is a validator macro, not a validator, and can not have a constraint value.",
220                             validatorType));
221 
222                 if (expandedMacros.containsKey(validatorType))
223                     throw new RuntimeException(String.format("Validator macro '%s' appears more than once.",
224                             validatorType));
225 
226                 expandedMacros.put(validatorType, true);
227 
228                 List<ValidatorSpecification> parsed = parse(expanded);
229 
230                 // Add the new validator specifications to the front of the queue, replacing the validator macro
231 
232                 for (int i = 0; i < parsed.size(); i++)
233                 {
234                     queue.add(i, parsed.get(i));
235                 }
236             } else
237             {
238                 result.add(head);
239             }
240         }
241 
242         return result;
243     }
244 
245     /**
246      * A code defining what the parser is looking for.
247      */
248     enum State
249     {
250 
251         /**
252          * The start of a validator type.
253          */
254         TYPE_START,
255         /**
256          * The end of a validator type.
257          */
258         TYPE_END,
259         /**
260          * Equals sign after a validator type, or a comma.
261          */
262         EQUALS_OR_COMMA,
263         /**
264          * The start of a constraint value.
265          */
266         VALUE_START,
267         /**
268          * The end of the constraint value.
269          */
270         VALUE_END,
271         /**
272          * The comma after a constraint value.
273          */
274         COMMA
275     }
276 
277     static List<ValidatorSpecification> parse(String specification)
278     {
279         List<ValidatorSpecification> result = newList();
280 
281         char[] input = specification.toCharArray();
282 
283         int cursor = 0;
284         int start = -1;
285 
286         String type = null;
287         boolean skipWhitespace = true;
288         State state = State.TYPE_START;
289 
290         while (cursor < input.length)
291         {
292             char ch = input[cursor];
293 
294             if (skipWhitespace && Character.isWhitespace(ch))
295             {
296                 cursor++;
297                 continue;
298             }
299 
300             skipWhitespace = false;
301 
302             switch (state)
303             {
304 
305                 case TYPE_START:
306 
307                     if (Character.isLetter(ch))
308                     {
309                         start = cursor;
310                         state = State.TYPE_END;
311                         break;
312                     }
313 
314                     parseError(cursor, specification);
315 
316                 case TYPE_END:
317 
318                     if (Character.isLetter(ch))
319                     {
320                         break;
321                     }
322 
323                     type = specification.substring(start, cursor);
324 
325                     skipWhitespace = true;
326                     state = State.EQUALS_OR_COMMA;
327                     continue;
328 
329                 case EQUALS_OR_COMMA:
330 
331                     if (ch == '=')
332                     {
333                         skipWhitespace = true;
334                         state = State.VALUE_START;
335                         break;
336                     }
337 
338                     if (ch == ',')
339                     {
340                         result.add(new ValidatorSpecification(type));
341                         type = null;
342                         state = State.COMMA;
343                         continue;
344                     }
345 
346                     parseError(cursor, specification);
347 
348                 case VALUE_START:
349 
350                     start = cursor;
351                     state = State.VALUE_END;
352                     break;
353 
354                 case VALUE_END:
355 
356                     // The value ends when we hit whitespace or a comma
357 
358                     if (Character.isWhitespace(ch) || ch == ',')
359                     {
360                         String value = specification.substring(start, cursor);
361 
362                         result.add(new ValidatorSpecification(type, value));
363                         type = null;
364 
365                         skipWhitespace = true;
366                         state = State.COMMA;
367                         continue;
368                     }
369 
370                     break;
371 
372                 case COMMA:
373 
374                     if (ch == ',')
375                     {
376                         skipWhitespace = true;
377                         state = State.TYPE_START;
378                         break;
379                     }
380 
381                     parseError(cursor, specification);
382             } // case
383 
384             cursor++;
385         } // while
386 
387         // cursor is now one character past end of string.
388         // Cleanup whatever state we were in the middle of.
389 
390         switch (state)
391         {
392             case TYPE_END:
393 
394                 type = specification.substring(start);
395 
396             case EQUALS_OR_COMMA:
397 
398                 result.add(new ValidatorSpecification(type));
399                 break;
400 
401             // Case when the specification ends with an equals sign.
402 
403             case VALUE_START:
404                 result.add(new ValidatorSpecification(type, ""));
405                 break;
406 
407             case VALUE_END:
408 
409                 result.add(new ValidatorSpecification(type, specification.substring(start)));
410                 break;
411 
412             // For better or worse, ending the string with a comma is valid.
413 
414             default:
415         }
416 
417         return result;
418     }
419 
420     private static void parseError(int cursor, String specification)
421     {
422         throw new RuntimeException(String.format("Unexpected character '%s' at position %d of input string: %s", specification.charAt(cursor), cursor + 1,
423                 specification));
424     }
425 }